/*
 * Created on May 1, 2007
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.networkmanager.admin.impl;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.config.impl.TransferSpeedValidator;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentFactory;
import org.gudy.azureus2.core3.util.AEThread;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.SystemProperties;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadManagerListener;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader;
import org.gudy.azureus2.pluginsimpl.local.PluginConfigImpl;
import org.gudy.azureus2.pluginsimpl.local.utils.resourcedownloader.ResourceDownloaderFactoryImpl;

import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduledTest;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduledTestListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduler;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTester;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTesterListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTesterResult;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.plugins.upnp.UPnPPlugin;

public class NetworkAdminSpeedTestScheduledTestImpl implements NetworkAdminSpeedTestScheduledTest {
    // Types of requests sent to SpeedTest scheduler.
    private static final long REQUEST_TEST = 0;
    private static final long CHALLENGE_REPLY = 1;
    private static final long TEST_RESULT = 2;

    private static int ZERO_DOWNLOAD_SETTING = -1;

    private PluginInterface plugin;
    private NetworkAdminSpeedTesterImpl tester;

    private String detectedRouter;

    private SpeedTestDownloadState preTestSettings;

    private byte[] challenge_id;
    private long delay_millis;
    private long max_speed;
    private TOTorrent test_torrent;

    private volatile boolean aborted;

    private CopyOnWriteList listeners = new CopyOnWriteList();

    protected NetworkAdminSpeedTestScheduledTestImpl(PluginInterface _plugin, NetworkAdminSpeedTesterImpl _tester) {
        plugin = _plugin;
        tester = _tester;

        // detect the router.

        PluginInterface upnp = plugin.getPluginManager().getPluginInterfaceByClass(UPnPPlugin.class);

        if (upnp != null) {

            detectedRouter = upnp.getPluginconfig().getPluginStringParameter("plugin.info");
        }

        tester.addListener(new NetworkAdminSpeedTesterListener() {
            public void complete(NetworkAdminSpeedTester tester, NetworkAdminSpeedTesterResult result) {
                try {
                    sendResult(result);

                } finally {

                    reportComplete();
                }
            }

            public void stage(NetworkAdminSpeedTester tester, String step) {
            }
        });
    }

    public NetworkAdminSpeedTester getTester() {
        return (tester);
    }

    public long getMaxUpBytePerSec() {
        return (max_speed);
    }

    public long getMaxDownBytePerSec() {
        return (max_speed);
    }

    public boolean start() {
        if (schedule()) {

            new AEThread("NetworkAdminSpeedTestScheduledTest:delay", true) {
                public void runSupport() {
                    long delay_ticks = delay_millis / 1000;

                    for (int i = 0; i < delay_ticks; i++) {

                        if (aborted) {

                            break;
                        }

                        String testScheduledIn =
                                MessageText.getString("SpeedTestWizard.abort.message.scheduled.in", new String[] { "" + (delay_ticks - i) });
                        reportStage(testScheduledIn);

                        try {
                            Thread.sleep(1000);

                        } catch (InterruptedException e) {

                            e.printStackTrace();
                        }
                    }

                    if (!aborted) {

                        setSpeedLimits();

                        if (tester.getTestType() == NetworkAdminSpeedTestScheduler.TEST_TYPE_BT) {

                            ((NetworkAdminSpeedTesterBTImpl) tester).start(test_torrent);

                        } else {
                            String unsupportedType = MessageText.getString("SpeedTestWizard.abort.message.unsupported.type");
                            tester.abort(unsupportedType);
                        }
                    }
                }
            }.start();

            return (true);

        } else {

            return (false);
        }
    }

    public void abort() {
        abort(MessageText.getString("SpeedTestWizard.abort.message.manual.abort"));
    }

    public void abort(String reason) {
        if (!aborted) {

            aborted = true;

            tester.abort(reason);
        }
    }

    /**
     * Request a test from the speed testing service, handle the "challenge" if request and then get the id for the test.
     * 
     * Per spec all request are BEncoded maps.
     * 
     * @return boolean - true if the test has been reserved with the service.
     */
    private boolean schedule() {
        try {
            // lookup UPnP devices found. One might be a router.

            // Send "schedule test" request.
            Map request = new HashMap();
            request.put("request_type", new Long(REQUEST_TEST));

            String id = COConfigurationManager.getStringParameter("ID", "unknown");

            // get jar file its version for the test

            File jar_file = null;
            String jar_version = null;

            String explicit_path = System.getProperty("azureus.speed.test.challenge.jar.path", null);

            if (explicit_path != null) {

                File f = new File(explicit_path);

                if (f.exists()) {

                    String v = getVersionFromJAR(f);

                    if (v != null) {

                        jar_file = f;
                        jar_version = v;

                        System.out.println("SpeedTest: using explicit challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version);
                    }
                }
            }

            if (jar_file == null) {

                String debug = System.getProperty("debug.speed.test.challenge", "n");

                if (!debug.equals("n")) {

                    // over-ride the jar version, and location for debugging.

                    File f = new File("C:\\test\\azureus\\Azureus3.0.1.2.jar"); // ToDo: make this a -D option with this default.

                    if (f.exists()) {

                        jar_file = f;
                        jar_version = "3.0.1.2";

                        System.out.println("SpeedTest: using old spec challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version);
                    }
                }
            }

            if (jar_file == null) {

                jar_file = FileUtil.getJarFileFromClass(getClass());

                if (jar_file != null) {

                    jar_version = Constants.AZUREUS_VERSION;

                    // System.out.println( "SpeedTest: using class-based challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version );

                } else {

                    File f = new File(SystemProperties.getAzureusJarPath());

                    if (f.exists()) {

                        jar_version = Constants.AZUREUS_VERSION;
                        jar_file = f;

                        // System.out.println( "SpeedTest: using config-based challenge jar " + jar_file.getAbsolutePath() + ", version " +
                        // jar_version );
                    }
                }
            }

            if (jar_file == null) {

                throw (new Exception("Failed to locate an 'Azureus2.jar' to use for the challenge protocol"));
            }

            // ToDo: remove once challenge testing is done.

            request.put("az-id", id); // Where to I get the AZ-ID and client version from the Configuration?
            request.put("type", "both");
            request.put("jar_ver", jar_version);

            if (detectedRouter != null) {

                request.put("router", detectedRouter);
            }

            Map result = sendRequest(request);

            challenge_id = (byte[]) result.get("challenge_id");

            if (challenge_id == null) {

                throw new IllegalStateException("No challenge returned from speed test scheduling service");
            }

            Long responseType = (Long) result.get("reply_type");

            if (responseType.intValue() == 1) {
                // a challenge has occured.
                result = handleChallengeFromSpeedTestService(jar_file, result);

                responseType = (Long) result.get("reply_type");
            }

            if (responseType == null) {
                throw new IllegalStateException("No challenge response returned from speed test scheduling service");
            }

            if (responseType.intValue() == 0) {
                // a test has been scheduled.
                // set the Map properly.

                Long time = (Long) result.get("time");
                Long limit = (Long) result.get("limit");

                if (time == null || limit == null) {
                    throw new IllegalArgumentException("Returned time or limit parameter is null");
                }

                delay_millis = time.longValue();
                max_speed = limit.longValue();

                // this is test-specific data

                Map torrentMap = (Map) result.get("torrent");

                test_torrent = TOTorrentFactory.deserialiseFromMap(torrentMap);

                return (true);
            } else {

                throw new IllegalStateException("Unrecognized response from speed test scheduling servcie.");
            }

        } catch (Throwable t) {

            Debug.printStackTrace(t);

            tester.abort(MessageText.getString("SpeedTestWizard.abort.message.scheduling.failed"), t);

            return (false);
        }
    }

    private String getVersionFromJAR(File jar_file) {
        try {
            // force the URLClassLoader to load from the URL and not delegate and find the currently
            // jar's Constants

            ClassLoader parent = new ClassLoader() {
                protected synchronized Class loadClass(String name, boolean resolve)

                throws ClassNotFoundException {
                    if (name.equals("org.gudy.azureus2.core3.util.Constants")) {

                        throw (new ClassNotFoundException());
                    }

                    return (super.loadClass(name, resolve));
                }
            };

            ClassLoader cl = new URLClassLoader(new URL[] { jar_file.toURI().toURL() }, parent);

            Class c = cl.loadClass("org.gudy.azureus2.core3.util.Constants");

            Field field = c.getField("AZUREUS_VERSION");

            return ((String) field.get(null));

        } catch (Throwable e) {

            return (null);
        }
    }

    /**
     * 
     * @param jar_file
     *            - File Azureus jar used to load classes.
     * @param result
     *            - Map from the previous response
     * @return Map - from the current response.
     */
    private Map handleChallengeFromSpeedTestService(File jar_file, Map result)

    throws IOException {
        // verify the following items are in the response.

        // size (in bytes)
        // offset (in bytes)
        // challenge_id
        Map retVal = new HashMap();
        RandomAccessFile raf = null;
        try {

            Long size = (Long) result.get("size");
            Long offset = (Long) result.get("offset");

            if (size == null || offset == null)
                throw new IllegalStateException("scheduleTestWithSpeedTestService had a null parameter.");

            // read the bytes
            raf = new RandomAccessFile(jar_file, "r");
            byte[] jarBytes = new byte[size.intValue()];

            raf.seek(offset.intValue());
            raf.read(jarBytes);

            // Build the URL.
            Map request = new HashMap();
            request.put("request_type", new Long(CHALLENGE_REPLY));
            request.put("challenge_id", challenge_id);
            request.put("data", jarBytes);

            retVal = sendRequest(request);

        } finally {
            // close
            try {
                if (raf != null)
                    raf.close();
            } catch (Throwable t) {
                Debug.printStackTrace(t);
            }
        }

        return retVal;
    }

    private void sendResult(NetworkAdminSpeedTesterResult result) {
        try {
            if (challenge_id != null) {

                Map request = new HashMap();

                request.put("request_type", new Long(TEST_RESULT));

                request.put("challenge_id", challenge_id);

                request.put("type", new Long(tester.getTestType()));
                request.put("mode", new Long(tester.getMode()));
                request.put("crypto", new Long(tester.getUseCrypto() ? 1 : 0));

                if (result.hadError()) {

                    request.put("result", new Long(0));

                    request.put("error", result.getLastError());

                } else {

                    request.put("result", new Long(1));

                    request.put("maxup", new Long(result.getUploadSpeed()));
                    request.put("maxdown", new Long(result.getDownloadSpeed()));
                }

                sendRequest(request);
            }
        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }
    }

    private Map sendRequest(Map request)

    throws IOException {
        request.put("ver", new Long(1));// request version
        request.put("locale", MessageText.getCurrentLocale().toString());

        String speedTestServiceName = System.getProperty("speedtest.service.ip.address", Constants.SPEED_TEST_SERVER);

        URL urlRequestTest =
                new URL("http://" + speedTestServiceName + ":60000/scheduletest?request="
                        + URLEncoder.encode(new String(BEncoder.encode(request), "ISO-8859-1"), "ISO-8859-1"));

        return (getBEncodedMapFromRequest(urlRequestTest));

    }

    /**
     * Read from URL and return byte array.
     * 
     * @param url
     *            -
     * @return byte[] of the results. Max size currently 100k.
     * @throws java.io.IOException
     *             -
     */
    private Map getBEncodedMapFromRequest(URL url) throws IOException {

        ResourceDownloader rd = ResourceDownloaderFactoryImpl.getSingleton().create(url);

        InputStream is = null;
        Map reply = new HashMap();
        try {
            is = rd.download();
            reply = BDecoder.decode(new BufferedInputStream(is));

            // all replys of this type contains a "result"
            Long res = (Long) reply.get("result");
            if (res == null)
                throw new IllegalStateException("No result parameter in the response!! reply=" + reply);
            if (res.intValue() == 0) {
                StringBuffer msg = new StringBuffer("Server failed. ");
                String error = new String((byte[]) reply.get("error"));
                String errDetail = new String((byte[]) reply.get("error_detail"));
                msg.append("Error: ").append(error);

                // detail is of no interest to the user
                // msg.append(" ,error detail: ").append(errDetail);

                Debug.outNoStack("SpeedCheck server returned an error: " + error + ", details=" + errDetail);

                throw new IOException(msg.toString());
            }
        } catch (IOException ise) {
            // rethrow this type of exception.
            throw ise;
        } catch (Throwable t) {
            Debug.out(t);
            Debug.printStackTrace(t);
        } finally {
            try {
                if (is != null)
                    is.close();
            } catch (Throwable e) {
                Debug.printStackTrace(e);
            }
        }
        return reply;
    }// getBytesFromRequest

    /**
     * Restore all the downloads the state before the speed test started.
     */
    protected synchronized void resetSpeedLimits() {
        if (preTestSettings != null) {

            preTestSettings.restoreLimits();

            preTestSettings = null;
        }
    }

    /**
     * Preserve all the data about the downloads while the test is running.
     */
    protected synchronized void setSpeedLimits() {

        // in case we've already saved limits

        resetSpeedLimits();

        preTestSettings = new SpeedTestDownloadState();

        preTestSettings.saveLimits();
    }

    // --------------- HELPER CLASSES BELOW HERE ---------------- //

    /**
     * Preservers the state of all the downloads before the speed test started.
     */
    class SpeedTestDownloadState implements ParameterListener, DownloadManagerListener {

        private Map torrentLimits = new HashMap(); // Map <Download , Map<String,Integer> >

        public static final String TORRENT_UPLOAD_LIMIT = "u";
        public static final String TORRENT_DOWNLOAD_LIMIT = "d";

        // global limits.

        int maxUploadKbs;
        int maxUploadSeedingKbs;
        int maxDownloadKbs;

        boolean autoSpeedEnabled;
        boolean autoSpeedSeedingEnabled;
        boolean LANSpeedEnabled;

        public SpeedTestDownloadState() {
        }

        public void parameterChanged(String name) {
            // add some trace so we have some clue as to what has made the change!

            String trace = Debug.getCompressedStackTrace();

            abort("Configuration parameter '" + name + "' changed (new value=" + COConfigurationManager.getParameter(name) + ") during test ("
                    + trace + ")");
        }

        public void downloadAdded(Download download) {
            if (test_torrent != null) {

                try {
                    if (Arrays.equals(download.getTorrent().getHash(), test_torrent.getHash())) {

                        return;
                    }
                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }

            String downloadAdded = MessageText.getString("SpeedTestWizard.abort.message.download.added", new String[] { download.getName() });
            abort(downloadAdded);
        }

        public void downloadRemoved(Download download) {
        }

        public void saveLimits() {
            // a bunch of plugins mess with limits (AutoSpeed, Shaper, SpeedScheduler...) - disable their
            // ability to mess with config during the test

            PluginConfigImpl.setEnablePluginCoreConfigChange(false);

            plugin.getDownloadManager().addListener(this, false);

            // preserve the limits for all the downloads and set each to zero.
            Download[] d = plugin.getDownloadManager().getDownloads();
            if (d != null) {
                int len = d.length;
                for (int i = 0; i < len; i++) {

                    plugin.getDownloadManager().getStats();
                    int downloadLimit = d[i].getDownloadRateLimitBytesPerSecond();
                    int uploadLimit = d[i].getUploadRateLimitBytesPerSecond();

                    setDownloadDetails(d[i], uploadLimit, downloadLimit);

                    d[i].setUploadRateLimitBytesPerSecond(ZERO_DOWNLOAD_SETTING);
                    d[i].setDownloadRateLimitBytesPerSecond(ZERO_DOWNLOAD_SETTING);
                }
            }

            // preserve the global limits

            saveGlobalLimits();

            COConfigurationManager.setParameter("LAN Speed Enabled", false);

            COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY, false);
            COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY, false);

            COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_CONFIGKEY, max_speed);
            COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY, max_speed);
            COConfigurationManager.setParameter(TransferSpeedValidator.DOWNLOAD_CONFIGKEY, max_speed);

            String[] params = TransferSpeedValidator.CONFIG_PARAMS;

            for (int i = 0; i < params.length; i++) {
                COConfigurationManager.addParameterListener(params[i], this);
            }
        }

        public void restoreLimits() {
            String[] params = TransferSpeedValidator.CONFIG_PARAMS;

            for (int i = 0; i < params.length; i++) {
                COConfigurationManager.removeParameterListener(params[i], this);
            }

            plugin.getDownloadManager().removeListener(this);

            restoreGlobalLimits();

            restoreIndividualLimits();

            PluginConfigImpl.setEnablePluginCoreConfigChange(true);
        }

        /**
         * Get the global limits from the TransferSpeedValidator class. Call before starting a speed test.
         */
        private void saveGlobalLimits() {
            // int settings.
            maxUploadKbs = COConfigurationManager.getIntParameter(TransferSpeedValidator.UPLOAD_CONFIGKEY);
            maxUploadSeedingKbs = COConfigurationManager.getIntParameter(TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY);
            maxDownloadKbs = COConfigurationManager.getIntParameter(TransferSpeedValidator.DOWNLOAD_CONFIGKEY);
            // boolean setting.
            autoSpeedEnabled = COConfigurationManager.getBooleanParameter(TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY);
            autoSpeedSeedingEnabled = COConfigurationManager.getBooleanParameter(TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY);

            LANSpeedEnabled = COConfigurationManager.getBooleanParameter("LAN Speed Enabled");

        }// saveGlobalLimits

        /**
         * Call this method after a speed test completes to restore the global limits.
         */
        private void restoreGlobalLimits() {
            COConfigurationManager.setParameter("LAN Speed Enabled", LANSpeedEnabled);

            COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY, autoSpeedEnabled);
            COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY, autoSpeedSeedingEnabled);

            COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_CONFIGKEY, maxUploadKbs);
            COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY, maxUploadSeedingKbs);
            COConfigurationManager.setParameter(TransferSpeedValidator.DOWNLOAD_CONFIGKEY, maxDownloadKbs);
        }// restoreGlobalLimits

        /**
         * Call this method after the speed test is completed to restore the individual download limits before the test started.
         */
        private void restoreIndividualLimits() {
            Download[] downloads = getAllDownloads();
            if (downloads != null) {
                int nDownloads = downloads.length;

                for (int i = 0; i < nDownloads; i++) {
                    int uploadLimit = getDownloadDetails(downloads[i], TORRENT_UPLOAD_LIMIT);
                    int downLimit = getDownloadDetails(downloads[i], TORRENT_DOWNLOAD_LIMIT);

                    downloads[i].setDownloadRateLimitBytesPerSecond(downLimit);
                    downloads[i].setUploadRateLimitBytesPerSecond(uploadLimit);
                }
            }
        }

        /**
         * Save the upload/download limits of this Download object before the test started.
         * 
         * @param d
         *            - Download
         * @param uploadLimit
         *            - int
         * @param downloadLimit
         *            - int
         */
        private void setDownloadDetails(Download d, int uploadLimit, int downloadLimit) {
            if (d == null)
                throw new IllegalArgumentException("Download should not be null.");

            Map props = new HashMap();// Map<String,Integer>

            props.put(TORRENT_UPLOAD_LIMIT, new Integer(uploadLimit));
            props.put(TORRENT_DOWNLOAD_LIMIT, new Integer(downloadLimit));

            torrentLimits.put(d, props);
        }

        /**
         * Get the upload or download limit for this Download object before the test started.
         * 
         * @param d
         *            - Download
         * @param param
         *            - String
         * @return - limit as int.
         */
        private int getDownloadDetails(Download d, String param) {
            if (d == null || param == null)
                throw new IllegalArgumentException("null inputs.");

            if (!param.equals(TORRENT_UPLOAD_LIMIT) && !param.equals(TORRENT_DOWNLOAD_LIMIT))
                throw new IllegalArgumentException("invalid param. param=" + param);

            Map out = (Map) torrentLimits.get(d);
            Integer limit = (Integer) out.get(param);

            return limit.intValue();
        }

        /**
         * Get all the Download keys in this Map.
         * 
         * @return - Download[]
         */
        private Download[] getAllDownloads() {
            Download[] a = new Download[0];
            return (Download[]) torrentLimits.keySet().toArray(a);
        }

    }

    protected void reportStage(String str) {
        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((NetworkAdminSpeedTestScheduledTestListener) it.next()).stage(this, str);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    protected void reportComplete() {
        resetSpeedLimits();

        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((NetworkAdminSpeedTestScheduledTestListener) it.next()).complete(this);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public void addListener(NetworkAdminSpeedTestScheduledTestListener listener) {
        listeners.add(listener);
    }

    public void removeListener(NetworkAdminSpeedTestScheduledTestListener listener) {
        listeners.remove(listener);
    }

}
